//
//  CWJavaScriptContext.m
//  CWDevelop
//
//  Created by LittoCats on 7/9/14.
//  Copyright (c) 2014 Littocats. All rights reserved.
//

#import "CWJavaScriptContext.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import <objc/message.h>
#import "CWCore.h"

#include <time.h>

#define JAVASCRIPTFUNCTIONARGUMENTSMAXCOUNT (size_t)8

@interface CWJavaScriptContext ()
{
    JSGlobalContextRef _JSContext;
    
    JSObjectRef _JavaScriptFunctionBridge;
}

@property (nonatomic,assign) JSGlobalContextRef JSContext;
@property (nonatomic,assign) JSObjectRef JavaScriptFunctionBridge;
@end

@implementation CWJavaScriptContext

+ (instancetype)context
{
    CWJavaScriptContext *context = [[self alloc] init];
    if (context->_JSContext == NULL) {
        context->_JSContext = JSGlobalContextCreate(NULL);
        
        //_id
        [context evaluateScript:[NSString stringWithFormat:@"_lang='zh';_debug=%@;_pid='%@';__pid = '%@';__api = '%@';",
                                 DEBUG_ON ? @"true" : @"false", context._id, kJavaScriptContextKey,kJavaScriptAPIKey]];
        
        //载入log方法
        JSStringRef logstr = JSStringCreateWithUTF8CString([@"kJavaScriptLog" cStringUsingEncoding:NSUTF8StringEncoding]);
        JSObjectRef logfunc = JSObjectMakeFunctionWithCallback(context->_JSContext, logstr, kCWJSLog);
        JSObjectSetProperty(context->_JSContext, JSContextGetGlobalObject(context->_JSContext), logstr, logfunc, kJSPropertyAttributeNone, NULL);
        JSStringRelease(logstr);
        
        //载入通道
        JSStringRef str = JSStringCreateWithUTF8CString([@"callDeviceApi" cStringUsingEncoding:NSUTF8StringEncoding]);
        JSObjectRef func = JSObjectMakeFunctionWithCallback(context->_JSContext, str, kCallDeviceApi);
        JSObjectSetProperty(context->_JSContext, JSContextGetGlobalObject(context->_JSContext), str, func, kJSPropertyAttributeNone, NULL);
        JSStringRelease(str);
        
        //加载库
        [context loadJSLibrary:@"promise.js"];
        [context loadJSLibrary:@"env_ios.js"];
        [context loadJSLibrary:@"api.js"];
        
        //载入appInfo
        [context evaluateScript:[NSString stringWithFormat:@"var app_info = %@",appInfoScript()]];
        
#if DEBUG_ON
        [context evaluateScript:[NSString stringWithFormat:@"console.log('jsContext init :','_pid = %@');",context._id]];
#endif
        JSStringRef jsFuncName = JSStringCreateWithUTF8CString([[NSString stringWithFormat:@"callJavaScriptFunction"] UTF8String]);//
        JSValueRef jsFuncValue = evaluateJavaScript(jsFuncName, context.JSContext);
        context->_JavaScriptFunctionBridge = JSValueIsObject(context->_JSContext, jsFuncValue) ? JSValueToObject(context->_JSContext, jsFuncValue, NULL) : NULL;
        
        JSStringRelease(jsFuncName);
    }
    return context;
}

- (id) evaluateScript:(NSString *)script
{
    if (!script) {
        NSLog(@"[ECJSContext] JS String is empty!");
        return nil;
    }
    JSStringRef scriptJS = JSStringCreateWithUTF8CString([script UTF8String]);
    
    NSObject *res = nil;
    JSValueRef result = evaluateJavaScript(scriptJS, _JSContext);
        res = NSObjectMake(result, _JSContext);
    
    JSStringRelease(scriptJS);
    return res;
}

JSValueRef evaluateJavaScript(JSStringRef script, JSGlobalContextRef context)
{
    JSValueRef exception = NULL;
    JSValueRef result = JSEvaluateScript(context, script, NULL, NULL, 0, &exception);
    
    if (!result) {
        if (exception) {
            JSStringRef exceptionArg = JSValueToStringCopy(context, exception, NULL);
            NSString* exceptionRes = (__bridge_transfer NSString*)JSStringCopyCFString(kCFAllocatorDefault, exceptionArg);
            
            JSStringRelease(exceptionArg);
            @throw [[NSException alloc] initWithName:@"[JSC] JavaScript exception" reason:exceptionRes userInfo:nil];
        }
        return NULL;
    }
    return result;
}

- (id)callFunction:(NSString *)func withArguments:(id)argument, ...
{
    JSValueRef arguments[JAVASCRIPTFUNCTIONARGUMENTSMAXCOUNT];
    int i = JAVASCRIPTFUNCTIONARGUMENTSMAXCOUNT;
    while (i) {
        arguments[--i] = NULL;
    }
    
    if (argument) {
        arguments[i ++] = JSValueMake(argument, _JSContext);
        
        va_list argp;
        va_start(argp, argument);
        id argt = nil;
        while(YES)
        {
            argt = va_arg(argp,id);
            if (!argt) break;
            arguments[i ++] = JSValueMake(argt, _JSContext);
        }
        va_end(argp);
    }
    
    size_t indexoffunction = JAVASCRIPTFUNCTIONARGUMENTSMAXCOUNT - 1;
    arguments[indexoffunction] = JSValueMake(func, _JSContext);
    
    JSValueRef result = callJavaScriptFunction(self, arguments);
    return NSObjectMake(result, _JSContext);
}

JSValueRef callJavaScriptFunction(CWJavaScriptContext *context, JSValueRef arguments[])
{
    JSValueRef exception = NULL;
    JSValueRef result = JSObjectCallAsFunction(context.JSContext, context.JavaScriptFunctionBridge, NULL, JAVASCRIPTFUNCTIONARGUMENTSMAXCOUNT, arguments, &exception);
    if (!result) {
        if (exception) {
            JSStringRef exceptionArg = JSValueToStringCopy(context.JSContext, exception, NULL);
            NSString* exceptionRes = (__bridge_transfer NSString*)JSStringCopyCFString(kCFAllocatorDefault, exceptionArg);
            
            JSStringRelease(exceptionArg);
            @throw [[NSException alloc] initWithName:@"[JSC] JavaScript exception" reason:exceptionRes userInfo:nil];
        }
        return NULL;
    }
    return result;
}
#pragma mark-
- (void)dealloc
{
    JSGlobalContextRelease(_JSContext);
}
#pragma mark- DQLog
JSValueRef kCWJSLog(JSContextRef ctx,
                   JSObjectRef function,
                   JSObjectRef thisObject,
                   size_t argumentCount,
                   const JSValueRef arguments[],
                   JSValueRef* exception)
{
//    CWLog(@"[JSLOG]  %@   %@",
//        [CWJavaScriptContext parseJSValue:arguments[0] inContext:ctx],
//        [CWJavaScriptContext parseJSValue:arguments[1] inContext:ctx]);
    CWLog(@"[JSLOG]  %@   %@",
          NSObjectMake(arguments[0], ctx),
          NSObjectMake(arguments[1], ctx));
    return JSValueMakeNull(ctx);
}
void dispatch_jsapi(NSString *method, id params, NSString *callBackId, NSMutableDictionary *action);
#pragma mark- DQ callDeviceApi
JSValueRef kCallDeviceApi(JSContextRef ctx,
                         JSObjectRef function,
                         JSObjectRef thisObject,
                         size_t argumentCount,
                         const JSValueRef arguments[],
                         JSValueRef* exception)
{
    NSString *method = NSObjectMake(arguments[0], ctx);
    id params = NSObjectMake(arguments[1], ctx);
    NSString *callBackId  = NSObjectMake(arguments[2], ctx);
    NSMutableDictionary *action = NSObjectMake(arguments[3], ctx);
    
    dispatch_jsapi(method, params, callBackId, action);
    return JSValueMakeNull(ctx);
}

#pragma mark- JSValueRef to NSObject
id NSObjectMake(JSValueRef jsvalue, JSContextRef context)
//+ (id)parseJSValue:(JSValueRef)jsvalue inContext:(JSContextRef)context
{
    JSValueRef exception = NULL;
    JSStringRef jsStringRef = NULL;
    NSString *jsString = nil;
    id result = nil;
    switch (JSValueGetType(context, jsvalue)) {
        case kJSTypeObject:
            jsStringRef = JSValueCreateJSONString(context, jsvalue, 0, &exception);
            jsString = (__bridge_transfer NSString*)JSStringCopyCFString(kCFAllocatorDefault, jsStringRef);
            result = [NSJSONSerialization JSONObjectWithData:[jsString dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil];
            
            JSStringRelease(jsStringRef);
            break;
        case kJSTypeString:
            jsStringRef = JSValueToStringCopy(context, jsvalue, &exception);
            jsString = (__bridge_transfer NSString*)JSStringCopyCFString(kCFAllocatorDefault, jsStringRef);
            result = jsString;
            JSStringRelease(jsStringRef);
            break;
        case kJSTypeBoolean:
            result = [NSNumber numberWithBool:JSValueToBoolean(context, jsvalue)];
            break;
        case kJSTypeNumber:
            result = [NSNumber numberWithDouble:JSValueToNumber(context, jsvalue, &exception)];
            break;
        case kJSTypeNull:
            result = [NSNull null];
            break;
        case kJSTypeUndefined:
            result = nil;
            break;
        default:
            break;
    }
    if (exception) {
        JSStringRef exceptionArg = JSValueToStringCopy(context, exception, NULL);
        NSString* exceptionRes = (__bridge_transfer NSString*)JSStringCopyCFString(kCFAllocatorDefault, exceptionArg);
        
        JSStringRelease(exceptionArg);
        CWLog(@"[JSC] JavaScript exception:\n\n*************** exception *************\n\n%@\n\n***************************\n\n", exceptionRes);
    }
    return result;
}
JSValueRef JSValueMake(id argument, JSContextRef context)
{
    JSValueRef value = NULL;
    if ([argument isKindOfClass:[NSString class]]) {
        JSStringRef jsString = JSStringCreateWithUTF8CString([argument UTF8String]);
        value =  JSValueMakeString(context, jsString);
        JSStringRelease(jsString);
    }else if ([argument isKindOfClass:[NSNumber class]]) {
        value = JSValueMakeNumber(context, [argument doubleValue]);
    }else if ([argument isKindOfClass:[NSDictionary class]] || [argument isKindOfClass:[NSArray class]]) {
        NSError *error;
        NSData *data = [NSJSONSerialization dataWithJSONObject:argument options:0 error:&error];
        JSStringRef jsString = JSStringCreateWithUTF8CString([[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] UTF8String]);
        value = JSValueMakeFromJSONString(context, jsString);
    }
    
    return value != NULL ? value : JSValueMakeNull(context);
}

#pragma mark- load js library
- (void)loadJSLibrary:(NSString *)libraryPath
{
    NSString *filePath = [NSString stringWithFormat:@"%@/cwjavascript_%@",[[NSBundle mainBundle] bundlePath],libraryPath];
    NSString *jsString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    [self evaluateScript:jsString ? jsString : @""];
}

- (NSString *)analyzeResourcePath:(NSString *)dir
{
    NSString *path;
    if ([dir hasPrefix:@"./"]) {
        path = dir.length > 2 ? [[self evaluateScript:@"__path"] stringByAppendingString:[dir substringFromIndex:2]] : [self evaluateScript:@"__path"];
    }else if ([dir hasPrefix:@"/lib"]){
        NSArray* paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
        path = dir.length > 4 ? [[paths firstObject] stringByAppendingString:[dir substringFromIndex:4]] : [paths firstObject];
    }else if ([dir hasPrefix:@"/home"]){
        path = dir.length > 5 ? [NSHomeDirectory() stringByAppendingString:[dir substringFromIndex:5]] : NSHomeDirectory();
    }else if ([dir hasPrefix:@"/temp"]){
        path = dir.length > 5 ? [NSTemporaryDirectory() stringByAppendingString:[dir substringFromIndex:5]] : NSTemporaryDirectory();
    }else if ([dir hasPrefix:[CWFile appRootPath]]){
        return dir;
    }else if ([dir hasSuffix:@"/"]){
        path = [[CWFile appRootPath] stringByAppendingString:dir];
    }else{
        return dir;
    }
    return path;
}

@end

@interface CWScriptFunction ()

@property (nonatomic, strong) NSString *functionId;
@property (nonatomic, strong) NSString *functionName;
@property (nonatomic, strong) NSString *contextId;

@end
@implementation CWScriptFunction

+ (instancetype)functionWithFunctionScript:(NSString *)func context:(NSString *)contextId
{
    CWScriptFunction *function = [[CWScriptFunction alloc] init];
    function.functionId = func;
    function.contextId = contextId;
    
    return function;
}
#pragma mark- evaluate
- (id)evaluateWithArguments:argument, ...
{
    CWJavaScriptContext *context = [NSObject findInstanceWithId:_contextId];
    
    JSValueRef arguments[JAVASCRIPTFUNCTIONARGUMENTSMAXCOUNT];
    int i = JAVASCRIPTFUNCTIONARGUMENTSMAXCOUNT;
    while (i) {
        arguments[--i] = NULL;
    }
    
    if (argument) {
        arguments[i ++] = JSValueMake(argument, context.JSContext);
        
        va_list argp;
        va_start(argp, argument);
        id argt = nil;
        while(YES)
        {
            argt = va_arg(argp,id);
            if (!argt) break;
            arguments[i ++] = JSValueMake(argt, context.JSContext);
        }
        va_end(argp);
    }
    
    size_t indexoffunction = JAVASCRIPTFUNCTIONARGUMENTSMAXCOUNT - 1;
    arguments[indexoffunction] = JSValueMake(self.functionId, context.JSContext);
    
    JSValueRef result = callJavaScriptFunction(context, arguments);
    return NSObjectMake(result, context.JSContext);
}
@end